#include "stdafx.h"
#include "Camera.h"
#include "Utility.h"

const float Camera::_epsilon = 0.0001f;

// default constructor... sets position to 0, 0, 5, aimed at the origin
// with the up vector set to the y axis
Camera::Camera()
{
	_position.set(0, 0, 5);
	_aim.set(0, 0, 0);
	_up.set(0, 1, 0);

	// set default view volume
	_nearPlane = 0.1f;
	_farPlane = 1000.0f;
	_fov = 60.0f;

	Initialize();
}

/* 
* constructor to set a camera to a desired orientation
* position is position in 3D
* aim is the aim coordinate
* up is the up vector 
*/
Camera::Camera(Vector3d position, Vector3d aim, Vector3d up)
{
	Vector3d dir = (aim-position).normalize();
	up = up.normalize();
	float dot = (float)(dir*up);

	// up vector and aim vector aren't perpendicular
	if (abs(dot) > _epsilon)
	{
		fprintf (stderr, "Improper camera orientation. Can't create camera!\n");
		this->~Camera();
		return;
	}
	else if (dir.z != 0.0)
	{
		up.z = -(up.x*dir.x + up.y*dir.y)/dir.z;
	}

	_position = position;
	_aim = aim;
	_up = up;

	// set default view volume
	_nearPlane = 0.1f;
	_farPlane = 1000.0f;
	_fov = 60.0f;

	Initialize();
}

/*
* Constructor setting up all values
*/
Camera::Camera(Vector3d position, Vector3d aim, Vector3d up, float nearPlane, float farPlane, float fov)
{

	// orientation of camera isn't perpendicular
	if ((aim-position).normalize() * up.normalize() > _epsilon)
	{
		fprintf (stderr, "Improper camera orientation. Can't create camera!\n");
		this->~Camera();
		return;
	}

	_position = position;
	_aim = aim;
	_up = up.normalize();

	_nearPlane = nearPlane;
	_farPlane = farPlane;
	_fov = fov;


	Initialize();
}


// Initialize routine setting up defaults
void Camera::Initialize()
{
	Vector3d tmp, tmp1, tmp2;
	Vector3d axisOrigin, updatePos;
	double dist;

	_mode = Inactive;

	// find the angle around the x axis 
	updatePos = _position - _aim;
	axisOrigin.set(updatePos.x, 0, 0);
	dist = (axisOrigin-updatePos).norm();
	tmp1.set(updatePos.x, 0, dist);

	tmp = updatePos.normalize();
	tmp1 = tmp1.normalize();

	_currentElevation = RadToDeg(acos(tmp*tmp1));

	// find the angle around the y axis
	axisOrigin.set(0, updatePos.y, 0);
	dist = (axisOrigin-updatePos).norm();

	tmp2.set(0, updatePos.y, dist);
	tmp2 = tmp2.normalize();

	_currentAzimuth = 360.0 - RadToDeg(acos(tmp2*tmp));

	_width = _height = 0;

	SetDefaults();
}

void Camera::SetDefaults()
{
	_defaultPosition = _position;
	_defaultAim = _aim;
	_defaultUp = _up;

	_defaultElevation = _currentElevation;
	_defaultAzimuth = _currentAzimuth;
}

// set functions for the _position, _aim, and _up vectors....
// be careful with these because if you set either of them which causes the
// orientation of the camera to be un-orthogonal, then you'll see problems...

// just remember that (_aim - _position).normalize() % _up == 0, or you'll see problems
void Camera::SetPosition(Vector3d position)
{
	_position = position;
}

void Camera::SetAim(Vector3d aim)
{
	_aim = aim;
}

void Camera::SetUp(Vector3d up)
{
	_up = up;
}

const Vector3d Camera::GetPosition(void) const
{
	return _position;
}

const Vector3d Camera::GetAim(void) const
{
	return _aim;
}

const Vector3d Camera::GetUp(void) const
{
	return _up;
}

/*
* sets the nearPlane and farPlane clipping planes for the camera view
*/
void Camera::SetClippingPlanes(float nearPlane, float farPlane)
{
	_nearPlane = nearPlane;
	_farPlane = farPlane;
}

/*
* sets the field of view of the camera, fov is in degrees
*/
void Camera::SetFov(float fov)
{
	_fov = fov;
}

const float Camera::GetFov(void) const
{
	return _fov;
}

/*
* resets the camera to its original orientation
*/
void Camera::Reset()
{
	_position = _defaultPosition;
	_aim = _defaultAim;
	_up = _defaultUp;

	_currentElevation = _defaultElevation;
	_currentAzimuth = _defaultAzimuth;
}

/*
* sets the camera's aim to be the given vector v
*/
void Camera::SetCenterOfFocus(Vector3d newAim)
{
	Vector3d dif = newAim - _aim;

	_aim = newAim;
	_position = _position + dif;
}

/*
* draws an opengl window with the camera orientation
* width and height are the width and height of the window respectively
*/
void Camera::PerspectiveDisplay(int width, int height) const
{
	PerspectiveDisplay(width, height, _fov);
}

void Camera::PerspectiveDisplay(int width, int height, float fov) const
{
	gluPerspective(fov, (float) width/(float) height, _nearPlane, _farPlane);
	gluLookAt(_position.x, _position.y, _position.z, _aim.x, _aim.y, _aim.z, _up.x, _up.y, _up.z);
}

void Camera::PerspectiveDisplayUpdate(int width, int height)
{
	PerspectiveDisplay(width, height);

	_width = width;
	_height = height;
}

void Camera::PerspectiveDisplayUpdate(int width, int height, float fov)
{
	PerspectiveDisplay(width, height, fov);

	_width = width;
	_height = height;
}


/*
* mouse event handler function... should be called in the 
* mouse event handler function of your own code
*/
void Camera::HandleMouseEvent(UINT buttonState, int x, int y)
{
	double realy, wx, wy, wz;

	// check to see if the ALT key has been used
	if ((buttonState == WM_LBUTTONUP || buttonState == WM_MBUTTONUP || buttonState == WM_RBUTTONUP) && _mode != Inactive)
	{
		// update the elevation and roll of the camera
		_currentElevation += _deltaElevation;
		_currentAzimuth += _deltaAzimuth;

		//printf("%f %f\n", _currentElevation, _currentAzimuth);

		// reset the change in elevation and roll of the camera
		_deltaElevation = _deltaAzimuth = 0.0;

		_mode = Inactive;
	} 
	else if (buttonState == WM_LBUTTONDOWN || buttonState == WM_MBUTTONDOWN || buttonState == WM_RBUTTONDOWN)
	{

		// set the new mouse state
		_mouseStartX = _mousePrevX = x;
		_mouseStartY = _mousePrevY = y;

		// alt key and mouse button have been pressed, camera will move

		switch (buttonState) 
		{
		case WM_LBUTTONDOWN:
			// rotating camera
			_mode = Rotate;
			break;
		case WM_MBUTTONDOWN:
			// translating camera:
			_mode = Translate;

			glPushAttrib(GL_TRANSFORM_BIT);
			{
				glMatrixMode(GL_PROJECTION);
				glPushMatrix();
				{
					PerspectiveDisplay(_width, _height);

					// get the modelview and projection matrices for projection
					// of the mouse's cursor into 3D
					glGetIntegerv(GL_VIEWPORT, _viewPort);
					glGetDoublev(GL_MODELVIEW_MATRIX, _modelViewMatrix);
					glGetDoublev(GL_PROJECTION_MATRIX, _projectionMatrix);
				}
				glPopMatrix();
			}
			glPopAttrib();

			// viewport[3] is height of window in pixels
			realy = _viewPort[3] - y - 1;

			// project the aim of the camera into window coordinates
			// only concerned about getting the depth (wz) from here
			gluProject(_aim.x, _aim.y, _aim.z, _modelViewMatrix, _projectionMatrix, _viewPort, &wx, &wy, &wz);

			// from the depth found from the previous call, project the
			// mouse coordinates into 3D coordinates
			gluUnProject((GLdouble) x, (GLdouble) realy, wz, _modelViewMatrix, _projectionMatrix, _viewPort, &_previousMousePosition.x, &_previousMousePosition.y, &_previousMousePosition.z);

			break;
		case WM_RBUTTONDOWN:
			// zooming camera:
			_mode = Zoom;
			break;
		}
	}
}

/*
* Mouse Motion handler function... should be called in the 
* mouse motion function of your own code
*/
void Camera::HandleMouseMotion(int x, int y)
{
	if (_mode != Inactive)
	{

		int mouse_dx, mouse_dy, d;
		double z;
		Vector3d MousePos, dir;
		Vector3d WindowX, WindowY, WindowZ;
		float realy;
		double wx, wy, wz;

		// find the greates change in mouse position 
		mouse_dx = x - _mousePrevX;
		mouse_dy = y - _mousePrevY;

		if (abs(mouse_dx) > abs(mouse_dy)) 
			d = mouse_dx;
		else
			d = mouse_dy;

		switch (_mode)
		{
		case Zoom:
			// camera is zooming in
			z = (double) d/100.0;

			dir = _aim - _position;

			if (dir.norm() < 0.1 && z > 0)
			{
				// move the aim position too when you get in really close
				z *= 10.0;
				_aim = _aim + z*dir;
			}

			// update the new position
			_position = _position + z*dir;
			break;
		case Rotate:
			// camera is rotating

			// get rate of change in screen coordinates from when the 
			// mouse was first pressed
			_deltaAzimuth = ((double) (x - _mouseStartX))/5.0;
			_deltaElevation = ((double) (y - _mouseStartY))/5.0;

			// get rate of change in screen coordinate from prev mouse pos
			_localDeltaAzimuth = ((double) mouse_dx)/5.0;
			_localDeltaElevation = ((double) mouse_dy)/5.0;

			// rotate the window coordinate system by the rate of change
			// from the onset of the mouse event

			// got this small section of code from Dr. House
			WindowX.set(1, 0, 0);
			WindowY.set(0, 1, 0);

			RotateX(&WindowX, _currentElevation+_deltaElevation);
			RotateY(&WindowX, _currentAzimuth+_deltaAzimuth);
			WindowX.z = -WindowX.z;

			RotateX(&WindowY, _currentElevation+_deltaElevation);
			RotateY(&WindowY, _currentAzimuth+_deltaAzimuth);
			WindowY.z = -WindowY.z;

			WindowZ = (WindowX%WindowY).normalize();

			ArbitraryRotate(WindowX, WindowY, WindowZ, _localDeltaElevation, 0, _position, _aim);

			ArbitraryRotate(Vector3d(1, 0, 0), Vector3d(0, 1, 0), Vector3d(0, 0, 1), 0, -_localDeltaAzimuth, _position, _aim);

			_up = WindowY.normalize();

			break;
		case Translate:
			// camera is translating

			realy = (float)(_viewPort[3] - y - 1);

			gluProject(_aim.x, _aim.y, _aim.z, _modelViewMatrix, _projectionMatrix, _viewPort, &wx, &wy, &wz);
			gluUnProject((GLdouble) x, (GLdouble) realy, wz, _modelViewMatrix, _projectionMatrix, _viewPort, &MousePos.x, &MousePos.y, &MousePos.z);

			// move both the camera position and its aim coordinate
			dir = MousePos - _previousMousePosition;
			_position = _position - dir;
			_aim = _aim - dir;

			_previousMousePosition = MousePos;
			break;
		}

		_mousePrevX = x;
		_mousePrevY = y;
	}
}


void Camera::RotateX(Vector3d *v, double degree)
{
	double c=cos(3.1415926*degree/180);
	double s=sin(3.1415926*degree/180);
	double v1=(*v)[1]*c-(*v)[2]*s;
	double v2=(*v)[1]*s+(*v)[2]*c;
	(*v)[1]=v1; (*v)[2]=v2;
}

void Camera::RotateY(Vector3d *v, double degree)
{
	double c=cos(3.1415926*degree/180);
	double s=sin(3.1415926*degree/180);
	double v0=(*v)[0]*c+(*v)[2]*s;
	double v2=-(*v)[0]*s+(*v)[2]*c;
	(*v)[0]=v0; (*v)[2]=v2;
}

/* 
* ArbitraryRotate() - rotate around an arbitrary coordinate system specified by
*                     up, V, & width
*/
void Camera::ArbitraryRotate(Vector3d u, Vector3d v, Vector3d w, double degreeX, double degreeY, Vector3d& point, Vector3d aim)
{
	double cx=cos(PI*degreeX/180);
	double sx=sin(PI*degreeX/180);
	double cy=cos(PI*degreeY/180);
	double sy=sin(PI*degreeY/180); 

	Matrix4x4 trans(1, 0, 0, -aim[0], 0, 1, 0, -aim[1], 0, 0, 1, -aim[2], 0, 0, 0, 1);
	Matrix4x4 mat(u[0], u[1], u[2], 0, v[0], v[1], v[2], 0, w[0], w[1], w[2], 0, 0, 0, 0, 1);
	Matrix4x4 rot;
	Vector4d pos(point[0], point[1], point[2], 1);

	pos = trans*pos;
	pos = mat*pos;

	rot.set(1, 0, 0, 0, 0,  cx, sx, 0, 0, -sx, cx, 0, 0,   0,  0, 1);
	pos = rot*pos;
	rot.set(cy, 0, sy, 0, 0, 1,  0, 0, -sy, 0, cy, 0, 0, 0,  0, 1);
	pos = rot*pos;
	pos = mat.inv()*pos;
	pos = trans.inv()*pos;

	point.set(pos[0], pos[1], pos[2]);
}

// equals operator
const Camera& Camera::operator=(const Camera& Cam)
{
	_aim = Cam._aim;
	_position = Cam._position;
	_up = Cam._up;

	return *this;
}
